/*
JETware Hands-free Extension provides additional features for Windows Mobile HFP support.
Copyright (C) 2004-2009 Jim Thatcher

This program is free software; you can redistribute it and/or modify it under the terms of the 
GNU General Public License as published by the Free Software Foundation; version 2 of the License.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program; 
if not, write to the Free Software Foundation, Inc., 
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

Source Control header

$Workfile: HandsFreeExt.cpp $
$Revision: 29 $
$Date: 7/19/07 1:13p $
*/
#include "stdafx.h"

#include <windows.h>
#include <winuserm.h>
#include "BTHFExt.h"
#include <pm.h>
#ifdef USE_NOTIFY
#include "NotifyMgr.h"
#endif

typedef BOOL (*PFN_ChainCommand) (LPSTR szCommand, DWORD cbCommand);
typedef void (*PFN_ChainSetCallback) (PFN_SendATCommand pfn);
#define VC_BUTTON_KEY		_T("ButtonApps\\Microsoft\\Voice Command")
#define OEM_VOICE_APP		_T("Software\\OEM\\VoiceCommand")
#define JETWAREAG_KEY		_T("SOFTWARE\\JETware\\AGext")
#define VC_PATH				_T("AppFilePath")
#define VC_ARGS				_T("Parameters")
#define OEM_PATH			_T("Path")
#define VOICEDIAL_PATH		_T("VoiceDialPath")

PFN_SendATCommand g_pfnSendATCommand = NULL;
PFN_ChainCommand g_pfnChainCommand = NULL;
HMODULE g_hChainModule = NULL;

DWORD g_dwMsgFormat = MSG_FORMAT_TEXT;
DWORD g_dwMSSelected = MESSAGES_SM; // 0=ME, 1=SM
DWORD g_dwPSSelected = CONTACTS_ME; // 0=ME, 1=SM
HANDLE g_hInst = NULL;
BOOL g_bReportStatus = FALSE;
BOOL g_bNeedExtraStatus = FALSE;
BOOL g_bDialingInitiated = FALSE;
BOOL g_bAnsweringCall = FALSE;
WORD g_wConvertToCharSet = CHARSET_GSM;
CRITICAL_SECTION csStatus;
CRITICAL_SECTION csCommand;
CRITICAL_SECTION csURQ;
// BOOL g_bMaskChainResponse = FALSE;
BOOL g_bIsSmartPhone = TRUE;
CHAR szOptions[] = {'=','?'};
CHAR PBLIST[8][3] = {{'M','E',0},{'S','M',0},{'D','C',0},{'R','C',0},{'M','C',0},{'L','D',0},{'F','D',0},{'O','N',0}};
CHAR MSLIST[2][3] = {{'M','E',0},{'S','M',0}};
CHAR szResponse[1024];
DWORD dwResponseLen = 0;
DWORD g_dwSessionThreadId = 0;
DWORD g_dwMainProcessId = 0;


CHAR * g_szInitiateVoiceDialNumber = NULL;

#ifdef PARROT
// if we are building only for Parrot, default is to not run - BRSF=31 can override
// BOOL bOKToRun = FALSE;
#endif

#ifdef DEBUG
void ReportPowerState();
#endif

WORD wLogFlag = 0;

#ifdef __cplusplus
extern "C" {
#endif

BOOL DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpVoid)
{
#ifdef DEBUG
		CHAR szLog[64];
#endif
	if (dwReason == DLL_PROCESS_ATTACH)
	{
		g_hInst = hModule;
#ifdef DEBUG
		sprintf(szLog, "DllMain PROCESS_ATTACH process=%8.8X", GetCurrentProcessId());
		g_dwDebugLevels |= DEBUG_INFO;
		LogString(szLog, DEBUG_INFO);
#endif
		return TRUE;
	}
	else if (dwReason == DLL_THREAD_DETACH)
	{
#ifdef DEBUG
		sprintf(szLog, "DllMain DLL_THREAD_DETACH, process=%8.8X, thread=%8.8X", GetCurrentProcessId(), GetCurrentThreadId());
		g_dwDebugLevels |= DEBUG_INFO;
		LogString(szLog, DEBUG_INFO);
#endif
		if (GetCurrentThreadId() == g_dwSessionThreadId)
		{
			CleanupSessionState();
		}
		return TRUE;
	}
	else if (dwReason == DLL_PROCESS_DETACH)
	{
#ifdef DEBUG
		sprintf(szLog, "DllMain DLL_PROCESS_DETACH, process=%8.8X, thread=%8.8X", GetCurrentProcessId(), GetCurrentThreadId());
		g_dwDebugLevels |= DEBUG_INFO;
		LogString(szLog, DEBUG_INFO);
#endif
		if (GetCurrentProcessId() == g_dwMainProcessId)
		{
			g_dwMainProcessId = 0;
			CleanupSIM();
			CleanupTAPI();
			CleanupMAPI();
			if (g_szMsgStore[0] != NULL)
			{
				LocalFree(g_szMsgStore[0]);
				g_szMsgStore[0] = NULL;
			}
			if (g_szMsgStore[0] != NULL)
			{
				LocalFree(g_szMsgStore[0]);
				g_szMsgStore[0] = NULL;
			}
			CleanupOutlook();
			CleanupSession();
			DeleteCriticalSection(&csStatus);
			DeleteCriticalSection(&csCommand);
			DeleteCriticalSection(&csMsgList);
			DeleteCriticalSection(&csURQ);
			if (g_hChainModule != NULL)
			{
				FreeLibrary(g_hChainModule);
				g_hChainModule = NULL;
			}
			if (g_szExtChainDll != NULL)
			{
				LocalFree(g_szExtChainDll);
				g_szExtChainDll = NULL;
			}
			if (g_szContactFilter != NULL)
			{
				LocalFree(g_szContactFilter);
				g_szContactFilter = NULL;
			}
			if (g_szInitiateVoiceDialNumber != NULL)
			{
				LocalFree(g_szInitiateVoiceDialNumber);
				g_szInitiateVoiceDialNumber = NULL;
			}
			if (g_szMfrString != NULL)
			{
				LocalFree(g_szMfrString);
				g_szMfrString = NULL;
			}
			if (g_szModelString != NULL)
			{
				LocalFree(g_szModelString);
				g_szModelString = NULL;
			}
		}

		return TRUE;
	}
	else
	{
#ifdef DEBUG
		sprintf(szLog, "DllMain REASON=%i, process=%8.8X, thread=%8.8X", dwReason, GetCurrentProcessId(), GetCurrentThreadId());
		g_dwDebugLevels |= DEBUG_INFO;
		LogString(szLog, DEBUG_INFO);
#endif
		return TRUE;
	}
}

#ifdef __cplusplus
}
#endif

DWORD ChainCallbackFn(LPSTR szCommand, DWORD cbCommand)
{
	DWORD dwReturn = 0;
	// EnterCriticalSection(&csCommand);
#ifdef DEBUG
	CHAR * szLog;
	szLog = (CHAR *) LocalAlloc(LPTR, cbCommand+32);
	sprintf(szLog, "[%s] Len=%d", szCommand, cbCommand);
	LogString(szLog, DEBUG_CHAIN);
	LocalFree(szLog);
#endif

	// if (!g_bMaskChainResponse)
	dwReturn = g_pfnSendATCommand(szCommand, cbCommand);
	return dwReturn;
}

void BthAGATSetCallback(PFN_SendATCommand pfn)
{
#ifdef DEBUG
	CHAR szLog[MAX_PATH+64];
#endif
	DWORD dwRet = 0;
	TCHAR szPlatform[32];

	if (SystemParametersInfo(SPI_GETPLATFORMTYPE, sizeof(szPlatform), szPlatform, 0))
	{
		if (lstrcmp(szPlatform, TEXT("PocketPC")) == 0)
			g_bIsSmartPhone = FALSE;
	}
	InitializeCriticalSection(&csStatus);
	InitializeCriticalSection(&csCommand);
	InitializeCriticalSection(&csMsgList);
	InitializeCriticalSection(&csURQ);
	GetRegSettings(); // Just need chain
	if (g_szExtChainDll != NULL)
	{
		PFN_ChainSetCallback ChainCallback = NULL;
		TCHAR szValueUpr[MAX_PATH];
		wcscpy(szValueUpr, g_szExtChainDll);
		_wcsupr(szValueUpr);

		if (wcsstr(szValueUpr, _T("JETBTHFX")) != NULL)
		{
#ifdef DEBUG
			LogString("Chain module loop detected", DEBUG_CHAIN);
			sprintf(szLog, "Attempting to chain to %S", g_szExtChainDll);
			LogString(szLog, DEBUG_CHAIN);
#endif
		}
		else
		{
			g_hChainModule = LoadLibrary(g_szExtChainDll);

			// TCHAR szChainPath[MAX_PATH];
			// TCHAR szMyPath[MAX_PATH];
			// DWORD dwRet = GetModuleFileName(hMod, szChainPath, MAX_PATH);
			// dwRet = GetModuleFileName(NULL, szMyPath, MAX_PATH);

			if (g_hChainModule != NULL)
			{
				ChainCallback = (PFN_ChainSetCallback) GetProcAddress(g_hChainModule, _T("BthAGATSetCallback"));
				if (ChainCallback != NULL)
				{
					ChainCallback(ChainCallbackFn);
#ifdef DEBUG
					sprintf(szLog, "Setup chain to %S", g_szExtChainDll);
					LogString(szLog, DEBUG_CHAIN);
#endif
				}
				g_pfnChainCommand = (PFN_ChainCommand) GetProcAddress(g_hChainModule, _T("BthAGATHandler"));
#ifdef DEBUG
				if (g_pfnChainCommand != NULL)
					LogString("Found chain command function", DEBUG_CHAIN);
#endif
			}

#ifdef DEBUG
			LogString("Initialized callback", DEBUG_CHAIN);
#endif
		}
	}
	g_pfnSendATCommand = pfn;


#ifdef DEBUG
	// g_bReportStatus = TRUE;
	// InitializeSIM();
#endif
	InitializeSession(FALSE);
	// InitializeMAPI();
}

void InitializeResponse()
{
	strcpy(szResponse, "\r\n");
	dwResponseLen = 2;
}

void AddLineToResponse(LPSTR szCommand, BOOL bFinal)
{
	DWORD dwCmdLen = strlen(szCommand);
	DWORD dwRet = 0;
#ifdef DEBUG
	CHAR szLog[64];
#endif

	if (g_dwShortResponse) // Force each line to be sent separately
		bFinal = TRUE;

	if ((g_dwFeatureMask & (FM_CS_PROTECT_RESP | FM_CS_NOPROTECT)) == FM_CS_PROTECT_RESP)
	{
#ifdef DEBUG
		LogString("Entering CriticalSection csCommand...", DEBUG_THREADING);
#endif
		EnterCriticalSection(&csCommand);
#ifdef DEBUG
		LogString("Entered CriticalSection csCommand", DEBUG_THREADING);
#endif
	}
	// Make sure command will fit in remaining part of buffer
	if ((dwResponseLen + dwCmdLen) > 1020)
	{
		/*
		if (g_dwFeatureMask & FM_FORCE_POWER_ON)
		{
			ResumeDevice();
		}
		*/
		LogString(szResponse, LOG_RESPONSES);
		dwRet = g_pfnSendATCommand(szResponse, dwResponseLen);
#ifdef DEBUG
		sprintf(szLog, "Sent partial response len=%i, ret=%i", dwResponseLen, dwRet);
		LogString(szLog, LOG_RESPONSES);
#endif
		szResponse[0] = 0;
		dwResponseLen = 0;
	}

	strcat(szResponse, szCommand);
	strcat(szResponse, "\r\n");
	dwResponseLen += (strlen(szCommand) + 2);

	if (bFinal)
	{
		/*
		if (g_dwFeatureMask & FM_FORCE_POWER_ON)
		{
			ResumeDevice();
		}
		*/
		LogString(szResponse, LOG_RESPONSES);
#ifdef DEBUG
		// CHAR * pResp = (CHAR *) LocalAlloc(LPTR, dwResponseLen + 5);
		// strcpy(pResp, szResponse);
		// dwRet = g_pfnSendATCommand(pResp, dwResponseLen);
#endif
		dwRet = g_pfnSendATCommand(szResponse, dwResponseLen);
#ifdef DEBUG
		sprintf(szLog, "Sent response len=%i, ret=%i", dwResponseLen, dwRet);
		LogString(szLog, LOG_RESPONSES);
		// LocalFree(pResp);
		if (dwRet != 0)
		{
			TCHAR szState[128];
			DWORD dwState = 0;
			DWORD dwRet = GetSystemPowerState(szState, 128, &dwState);
			if (dwRet == ERROR_SUCCESS)
			{
				sprintf(szLog, "Power state at error=%8.8X - %S", dwState, szState);
				LogString(szLog, LOG_RESPONSES);
			}
		}
#endif
		szResponse[0] = 0;
		dwResponseLen = 0;
	}
	if ((g_dwFeatureMask & (FM_CS_PROTECT_RESP | FM_CS_NOPROTECT)) == FM_CS_PROTECT_RESP)
		LeaveCriticalSection(&csCommand);
}

// Use SendATCommand for unsolicited responses
void SendATCommand(LPSTR szCommand)
{
	DWORD dwCount = strlen(szCommand);
	DWORD dwRet = 0;
	CHAR * pReply = (CHAR *) LocalAlloc(LPTR, sizeof(CHAR) * (dwCount + 10));
#ifdef DEBUG
	CHAR szLog[64];
	ReportPowerState();
#endif
	if ((g_dwFeatureMask & (FM_CS_PROTECT_RESP | FM_CS_NOPROTECT)) == FM_CS_PROTECT_RESP)
		EnterCriticalSection(&csCommand);
	if (pReply != NULL)
	{
		/*
		if (g_dwFeatureMask & FM_FORCE_POWER_ON)
		{
			ResumeDevice();
		}
		*/
		sprintf(pReply, "\r\n%s\r\n", szCommand);
		dwRet = g_pfnSendATCommand(pReply, dwCount + 4);
		LocalFree(pReply);
#ifdef DEBUG
		sprintf(szLog, "Sent response len=%i, ret=%i", dwCount + 4, dwRet);
		LogString(szLog, LOG_RESPONSES);
		if (dwRet != 0)
		{
			TCHAR szState[128];
			DWORD dwState = 0;
			DWORD dwRet = GetSystemPowerState(szState, 128, &dwState);
			if (dwRet == ERROR_SUCCESS)
			{
				sprintf(szLog, "Power state at error=%8.8X - %S", dwState, szState);
				LogString(szLog, LOG_RESPONSES);
			}
		}
#endif
	}
	else
	{
		g_pfnSendATCommand("\r\n", 2);
		g_pfnSendATCommand(szCommand, dwCount);
		g_pfnSendATCommand("\r\n", 2);
	}
	if ((g_dwFeatureMask & (FM_CS_PROTECT_RESP | FM_CS_NOPROTECT)) == FM_CS_PROTECT_RESP)
		LeaveCriticalSection(&csCommand);
}

void ConnectAudio()
{
	if (hBAG != INVALID_HANDLE_VALUE)
	{
		// Don't try to connect Audio when there is an A2DP connection active
		HKEY	hKey				= NULL;
		DWORD   dwType              = 0;
		DWORD	dwSize				= 0;
		LONG	lRet				= 0;
		DWORD	dwValue				= 0;
#ifdef DEBUG
		CHAR szLog[32];
#endif

		lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, _T("System\\State\\Hardware"), 0, 0, &hKey);
		if (ERROR_SUCCESS == lRet)
		{
			dwSize = sizeof(dwValue);
			dwType = 0;
			lRet = RegQueryValueEx(hKey, _T("Bluetooth"), NULL, &dwType, (LPBYTE) &dwValue, &dwSize);
			if (ERROR_SUCCESS == lRet)
			{
				if (dwSize && (dwType == REG_DWORD))
				{
					if (dwValue & 0x04)
					{
						// A2DP is active
#ifdef DEBUG
						sprintf(szLog, "Bluetooth A2DP already connected: %i", dwValue);
						LogString(szLog, DEBUG_INBANDRING | DEBUG_VOICEDIAL);
#endif
					}
					else
					{
						int n = DeviceIoControl(hBAG,IOCTL_AG_OPEN_AUDIO,NULL,0,NULL,0,NULL,NULL);
#ifdef DEBUG
						sprintf(szLog, "ConnectAudio returned %i, GetLastError = 0x%8.8X", n, GetLastError());
						LogString(szLog, DEBUG_INBANDRING | DEBUG_VOICEDIAL);
#endif
					}
				}
			}
			RegCloseKey(hKey);
		}
	}
}

void DisconnectAudio()
{
	if (hBAG != INVALID_HANDLE_VALUE)
	{
		// Don't try to disconnect Audio when there is an A2DP connection active
		HKEY	hKey				= NULL;
		DWORD   dwType              = 0;
		DWORD	dwSize				= 0;
		LONG	lRet				= 0;
		DWORD	dwValue				= 0;
#ifdef DEBUG
		CHAR szLog[32];
#endif

		lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, _T("System\\State\\Hardware"), 0, 0, &hKey);
		if (ERROR_SUCCESS == lRet)
		{
			dwSize = sizeof(dwValue);
			dwType = 0;
			lRet = RegQueryValueEx(hKey, _T("Bluetooth"), NULL, &dwType, (LPBYTE) &dwValue, &dwSize);
			if (ERROR_SUCCESS == lRet)
			{
				if (dwSize && (dwType == REG_DWORD))
				{
					if (dwValue & 0x04)
					{
						// A2DP is active
#ifdef DEBUG
						sprintf(szLog, "Bluetooth A2DP already connected: %i", dwValue);
						LogString(szLog, DEBUG_INBANDRING | DEBUG_VOICEDIAL);
#endif
					}
					else
					{
						int n = DeviceIoControl(hBAG,IOCTL_AG_CLOSE_AUDIO,NULL,0,NULL,0,NULL,NULL);
#ifdef DEBUG
						sprintf(szLog, "DisconnectAudio returned %i", n);
						LogString(szLog, DEBUG_INBANDRING | DEBUG_VOICEDIAL);
#endif
					}
				}
			}
			RegCloseKey(hKey);
		}
	}
}

void SendKeys(CHAR * pKeys, int nCount)
{
	for(int i=0; i < nCount; i++)
	{
		keybd_event(*(pKeys + i), 0, KEYEVENTF_SILENT, 0);
		keybd_event(*(pKeys + i), 0, KEYEVENTF_SILENT | KEYEVENTF_KEYUP, 0);
		Sleep(10);
	}
}

#ifdef DEBUG
void ReportPowerState()
{
	if (g_dwDebugLevels  & DEBUG_THREADING)
	{
		TCHAR szState[128];
		DWORD dwState = 0;
		DWORD dwRet = GetSystemPowerState(szState, 128, &dwState);
		if (dwRet == ERROR_SUCCESS)
		{
			CHAR szLog[256];
			sprintf(szLog, "ReportPowerState=%8.8X - %S", dwState, szState);
			LogString(szLog, DEBUG_THREADING);
		}
	}
}
#endif

void ResumeDevice()
{
	TCHAR szState[128];
	DWORD dwState = 0;
	DWORD dwRet = GetSystemPowerState(szState, 128, &dwState);
	if (dwRet == ERROR_SUCCESS)
	{
#ifdef DEBUG
		CHAR szLog[256];
		sprintf(szLog, "Power state=%8.8X - %S", dwState, szState);
		LogString(szLog, DEBUG_INFO);
#endif
		if (!(dwState & POWER_STATE_ON) && (wcsicmp(g_szPowerResumeName, szState) != 0))
		{
			SetSystemPowerState(g_szPowerResumeName, 0, TRUE);
#ifdef DEBUG
			sprintf(szLog, "Set power to %S", g_szPowerResumeName);
			LogString(szLog, DEBUG_INFO);
#endif
			/*
			if (g_dwFeatureMask & FM_FORCE_POWER_ON)
			{
				int n = 0;
				for (n=0; n < 10; n++)
				{
					GetSystemPowerState(szState, 128, &dwState);
					if (wcsicmp(g_szPowerResumeName, szState) == 0)
						break;
					Sleep(100*n+1);
				}
#ifdef DEBUG
				sprintf(szLog, "Resumed to %S after waiting %i times", szState, n);
				LogString(szLog, DEBUG_INFO);
#endif
			}
			*/
		}
	}
}

void SuspendDevice()
{
	TCHAR szState[128];
	DWORD dwState = 0;
	DWORD dwRet = GetSystemPowerState(szState, 128, &dwState);
	if (dwRet == ERROR_SUCCESS)
	{
#ifdef DEBUG
		CHAR szLog[256];
		sprintf(szLog, "Suspending - power state=%8.8X - %S", dwState, szState);
		LogString(szLog, DEBUG_VOICEDIAL);
#endif
		if ((dwState & POWER_STATE_ON))
		{
			// TODO: add check for not SmartPhone 
			SetSystemPowerState(g_szPowerSuspendName, 0, FALSE);
		}
	}
}

BOOL InitiateVoiceDial()
{
	PROCESS_INFORMATION pi;
	BOOL bRet = FALSE;
	if (g_dwHandleCKPD == CMD_INTERNAL_ONLY)
		return FALSE;
	else
	{
		HKEY    hKey                = NULL;
		DWORD   dwType              = 0;
		DWORD	dwSize				= 0;
		TCHAR	szPath[256];
		TCHAR	szArgs[32];
		LONG	lRet				= 0;
#ifdef DEBUG
		CHAR szVcLog[256];
#endif

		szArgs[0] = 0;
		lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, JETWAREAG_KEY, 0, 0, &hKey);
		if (ERROR_SUCCESS == lRet)
		{
			// Attempt to launch VoiceDialApp from JETware key if one is registered
			// read executable path
			dwSize = sizeof(szPath);
			dwType = 0;
			lRet = RegQueryValueEx(hKey, VOICEDIAL_PATH, NULL, &dwType, (LPBYTE) &szPath, &dwSize);
			if (ERROR_SUCCESS == lRet)
			{
				if (dwSize && (dwType == REG_SZ))
				{
					dwSize = sizeof(szArgs);
					dwType = 0;
					lRet = RegQueryValueEx(hKey, VC_ARGS, NULL, &dwType, (LPBYTE) &szArgs, &dwSize);
					if (ERROR_SUCCESS == lRet)
					{
						if (!dwSize || (dwType != REG_SZ))
						{
							szArgs[0] = 0;
						}
					}
#ifdef DEBUG
					sprintf(szVcLog, "CreateProc %ls %ls", szPath, szArgs);
					LogString(szVcLog, DEBUG_VOICEDIAL);
#endif
					ResumeDevice();
					ConnectAudio();
					bRet = CreateProcess(szPath, szArgs, NULL, NULL, NULL, 0, NULL, NULL, NULL, &pi);
				}
			}
			RegCloseKey(hKey);
		}
		if (!bRet)
		{
			lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, VC_BUTTON_KEY, 0, 0, &hKey);
			if (ERROR_SUCCESS == lRet)
			{
				// Attempt to launch Voice Command if it is installed
				// For VC 1.6 call voicecmd.exe -BTHPTT
				// read executable path
				dwSize = sizeof(szPath);
				dwType = 0;
				lRet = RegQueryValueEx(hKey, VC_PATH, NULL, &dwType, (LPBYTE) &szPath, &dwSize);
				if (ERROR_SUCCESS == lRet)
				{
					if (dwSize && (dwType == REG_SZ))
					{
						// Check version for 1.6
						DWORD dwHandle = 0;
						DWORD dwSize = 0;
						UINT iLen = 0;
						// TCHAR szVer[32];
						VOID * pVerData = NULL;
						VOID * pBlock = NULL;
						VS_FIXEDFILEINFO * pVerInfo = NULL;
						DWORD dwVersion = 0;

						dwSize = GetFileVersionInfoSize(szPath, &dwHandle);
						if (dwSize) // Read version info from App
						{
							pVerData = LocalAlloc(LPTR, dwSize);
							if (pVerData != NULL)
							{
								if (GetFileVersionInfo(szPath, dwHandle, dwSize, pVerData))
								{
									if (VerQueryValue(pVerData, _T("\\"), &pBlock, &iLen))
									{
										pVerInfo = (VS_FIXEDFILEINFO *) pBlock;
										dwVersion = (HIWORD(pVerInfo->dwProductVersionMS) * 100) + LOWORD(pVerInfo->dwProductVersionMS);
#ifdef DEBUG
										sprintf(szVcLog, "Voice Command product version %i.%i = %i", HIWORD(pVerInfo->dwProductVersionMS), 
											LOWORD(pVerInfo->dwProductVersionMS), dwVersion);
										LogString(szVcLog, DEBUG_VOICEDIAL);
										sprintf(szVcLog, "Voice Command file version %i.%i.%i.%i", HIWORD(pVerInfo->dwFileVersionMS), 
											LOWORD(pVerInfo->dwFileVersionMS), HIWORD(pVerInfo->dwFileVersionLS),
											LOWORD(pVerInfo->dwFileVersionLS));
										LogString(szVcLog, DEBUG_VOICEDIAL);
#endif
									}
								}
								LocalFree(pVerData);
							}
						}

						dwSize = sizeof(szArgs);
						dwType = 0;
						lRet = RegQueryValueEx(hKey, VC_ARGS, NULL, &dwType, (LPBYTE) &szArgs, &dwSize);
						if ((ERROR_SUCCESS != lRet) || !dwSize || (dwType != REG_SZ))
						{
							if (dwVersion < 160)
								wcscpy(szArgs, _T("-ptt"));
						}
						if (dwVersion >= 160)
							wcscpy(szArgs, _T("-BTHPTT"));
#ifdef DEBUG
						sprintf(szVcLog, "CreateProc %ls %ls", szPath, szArgs);
						LogString(szVcLog, DEBUG_VOICEDIAL);
#endif
						ResumeDevice();
						ConnectAudio();
						bRet = CreateProcess(szPath, szArgs, NULL, NULL, NULL, 0, NULL, NULL, NULL, &pi);
					}
				}
				RegCloseKey(hKey);
			}
		}
		if (!bRet)
		{	// Didn't find Voice Command, try OEM registered voice app
			lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, OEM_VOICE_APP, 0, 0, &hKey);
			if (ERROR_SUCCESS == lRet)
			{
				// On PPC, voice dialer app is listed in HKLM\\Software\\OEM\\VoiceCommand "Path"
				// read executable path
				dwSize = sizeof(szPath);
				dwType = 0;
				lRet = RegQueryValueEx(hKey, OEM_PATH, NULL, &dwType, (LPBYTE) &szPath, &dwSize);
				if (ERROR_SUCCESS == lRet)
				{
					if (dwSize && (dwType == REG_SZ))
					{
#ifdef DEBUG
						sprintf(szVcLog, "CreateProc %ls", szPath);
						LogString(szVcLog, DEBUG_VOICEDIAL);
#endif
						ResumeDevice();
						ConnectAudio();
						bRet = CreateProcess(szPath, szArgs, NULL, NULL, NULL, 0, NULL, NULL, NULL, &pi);
					}
				}
				RegCloseKey(hKey);
			}
		}

#ifdef DEBUG
		sprintf(szVcLog, "CreateProcess for voice dialer returned %i", bRet);
		LogString(szVcLog, DEBUG_VOICEDIAL);
#endif
	}
	return bRet;
}

BOOL BthAGATHandler(LPSTR szCommand, DWORD cbCommand)
{
	BOOL bChainRet = FALSE;
	CHAR szReply[512];
	BOOL bReturn = FALSE;
	DWORD dwErr = ERR_NOERR;
	BOOL bExtraCRLF = FALSE;

	CHAR szCmd[5];
	DWORD * pdwATCmd = (DWORD *) &szCmd;
	// trim front of command to start at "AT..."
	//FIX: 1.22: Stefan Misterek problem with Audio Blue device that has pre-command garbage
	for (DWORD i=0; i < cbCommand; i++)
	{
		if (*szCommand >= 'A')
			break;
		szCommand++;
	}
	CHAR * pTail = szCommand+7;  // whatever is behind 4 chars of main command

#ifdef DEBUG
	CHAR szLog[256];
#endif
	LogString(szCommand, LOG_REQUESTS);
	// Make sure we are only processing one command at a time
	if (!(g_dwFeatureMask & (FM_CS_PROTECT_RESP | FM_CS_NOPROTECT)))
		EnterCriticalSection(&csCommand); // TEST: 1.21: Protecting response queue instead - it's OK to have unsolicited response within command response
	// create a DWORD made up of the 4 chars of the main command
	strncpy(szCmd, szCommand+3, 4);
	szCmd[4] = 0;
	_strupr(szCmd);

#ifdef DEBUG
	sprintf(szReply, "%s (0x%8.8X) L=%d", szCommand, *pdwATCmd, cbCommand);
	LogString(szReply, LOG_REQUESTS);
	ReportPowerState();
#endif

	if (cbCommand >= 7) // not sure we can trust cbCommand, we get some calls that satisfy this test yet have no data
	{
		szReply[0] = 0;  // clear the reply string
		InitializeResponse();
		if (GetCurrentProcessId() != g_dwMainProcessId)
		{
			g_dwMainProcessId = GetCurrentProcessId();
		}
		if (GetCurrentThreadId() != g_dwSessionThreadId)
		{
			g_dwSessionThreadId = GetCurrentThreadId();
			if (!bInSession)
			{
				if (g_dwFeatureMask & FM_EARLY_REGREAD)
					GetRegSettings();
				InitializeSession(TRUE);
			}
		}
		switch (*pdwATCmd)
		{
			// Handle things we just want to say OK for
#ifdef HCB700
			case SEAM:
				sprintf(szReply, "*SEAM: 4401\r\n");
				wLogFlag = DEBUG_INFO;
				dwErr = ERR_OK;
				bReturn = TRUE;
				PostResponse("*SEAAI: 4401");
				break;
#endif
			case BRSF:
			{
				// TODO: Map HF device features to extension features
				int nCapabilities = 0;
				sscanf(pTail, "=%i", &nCapabilities);
				// InitializeSession(TRUE); // FIX 1.22
				break;
			}
			case BLDN:
			{
				g_dwCallSetup = CALLSETUP_OUTRING;
				break;
			}
			case CLAN:
			{
				TCHAR szLang[16];
				wLogFlag = DEBUG_INFO;
				if (*pTail == QUERY) // Query 
				{
					if (GetLocaleInfo(LOCALE_SYSTEM_DEFAULT, LOCALE_SABBREVLANGNAME, (LPTSTR) &szLang, (sizeof(szLang)/sizeof(TCHAR))))
					{
						dwErr = ERR_OK;
						bReturn = TRUE;
						szLang[2] = 0;
						_wcslwr(szLang);
						sprintf(szReply, "+CLAN: \"%ls\"\r\n", szLang);
						bExtraCRLF = TRUE;
					}
				}
				break;
			}
/*
			case CHLD:
			{
				// TODO: Add registry option to handle CHLD or leave to internal
				dwErr = ERR_OK;
				bReturn = TRUE;
				DWORD dwRet = 0;
				switch (*(pTail+1))
				{
					case '?': // query supported options
					{
						strcpy(szReply, "+CHLD: (0,1,2)");
						break;
					}
					case '1':
					{ // release active call(s), answer waiting/held call
						// SendKeys("p", 1); // "p" = VK_TSOFT1, "Accept"
						dwRet = BthAGNetworkDropCall(NETWORK_FLAGS_DROP_ACTIVE);
						dwRet = BthAGNetworkAnswerCall();
#ifdef DEBUG
						sprintf(szLog, "BthAGNetworkAnswerCall returned 0x%8.8X", dwRet);
						LogString(szLog, DEBUG_TAPI);
#endif
						break;
					}
					case '2':
					{ // place active calls on hold, answer waiting/held call
						// SendKeys("p", 1);
						dwRet = BthAGNetworkSwapCall();
#ifdef DEBUG
						sprintf(szLog, "BthAGNetworkSwapCall returned 0x%8.8X", dwRet);
						LogString(szLog, DEBUG_TAPI);
#endif
						break;
					}
					default:
					{
						SendKeys("q", 1); // "q" = VK_TSOFT2, "Decline"
						break;
					}
				}
				break;
			} // CHLD
*/
			case EIPS:
			{
				// Extended caller ID supported by Tekkeon EZ3000
				// T610 spports 2 lines
				// Check registry option to see if we should handle CLIP or leave to internal
				if (g_dwHandleCLIP == CMD_INTERNAL_ONLY)
					break;
				else if (g_dwHandleCLIP == CMD_LOCAL_ONLY)
				{
					dwErr = ERR_OK;
					bReturn = TRUE;
				}
				wLogFlag = DEBUG_INFO;
				if (*pTail == QUERY) // Query 
				{
					// for now, only supporting one line
					sprintf(szReply, "*EIPS: 1, %i", g_bReportECID);
				}
				else if (*pTail == '=')
				{
					if (*(pTail+1) == '?') // Options
					{
						strcpy(szReply, "*EIPS: 1, (0-1)");
					}
					else // Set
					{
						g_bReportECID = (*(pTail+3) == '1') ? TRUE : FALSE;
#ifdef USE_NOTIFY_CID
						if (g_pNotify)
						{
							if (g_bReportECID)
								g_pNotify->StartCallerID(JET_REPORT_ELIP);
							else
								g_pNotify->StopCallerID(JET_REPORT_ELIP);
						}
#endif
#ifdef DEBUG
						sprintf(szLog, "Set ReportECID flag to %i", g_bReportECID);
						LogString(szLog, wLogFlag);
#endif
					}
				}

				break;
			}
			case CLIP:
			{
				// Check registry option to see if we should handle CLIP or leave to internal
				if (g_dwHandleCLIP == CMD_INTERNAL_ONLY)
					break;
				else if (g_dwHandleCLIP == CMD_LOCAL_ONLY)
				{
					dwErr = ERR_OK;
					bReturn = TRUE;
				}
				wLogFlag = DEBUG_INFO;
				if (*pTail == QUERY) // Query 
				{
					sprintf(szReply, "+CLIP:%i,1", g_bReportCID);
				}
				else if (*pTail == '=')
				{
					if (*(pTail+1) == '?') // Options
					{
						strcpy(szReply, "+CLIP:0,1");
					}
					else // Set
					{
						g_bReportCID = (*(pTail+1) == '1') ? TRUE : FALSE;
#ifdef USE_NOTIFY_CID
						if (g_pNotify)
						{
							if (g_bReportCID)
								g_pNotify->StartCallerID(JET_REPORT_CLIP);
							else
								g_pNotify->StopCallerID(JET_REPORT_CLIP);
						}
#endif
#ifdef DEBUG
						sprintf(szLog, "Set ReportCID flag to %i", g_bReportCID);
						LogString(szLog, wLogFlag);
#endif
					}
				}

				break;
			}
			case CHUP:
			{ // if there is an incoming or held call, don't hangup, swap calls
				// Check registry option to see if we should handle CLIP or leave to internal
				// Only CMD_INTERNAL_ONLY and CMD_BOTH make sense here, CMD_LOCAL_ONLY treated as CMD_BOTH
				if (g_dwHandleCHUP == CMD_INTERNAL_ONLY)
					break;
				wLogFlag = DEBUG_INFO;
				if ((g_dwCallSetup != 0) && (g_bInCall))
				{
					// There is an incoming call, answer it and place active call on hold
					dwErr = ERR_OK;
					bReturn = TRUE;
					SendKeys("r", 1); // Simulate pressing VK_TTALK to Accept incoming call
					// dwRet = BthAGNetworkSwapCall();
#ifdef DEBUG
					// sprintf(szLog, "BthAGNetworkAnswerCall returned 0x%8.8X", dwRet);
					strcpy(szLog, "Swapping to incoming call ...");
					LogString(szLog, DEBUG_INFO);
#endif
				}
				else if (NumHeldCalls() > 0)
				{
					// Disconnect active call, transfer back to held call
					dwErr = ERR_OK;
					bReturn = TRUE;
					SendKeys("s", 1); // Simulate pressing VK_TEND to end active call, swap to held call
#ifdef DEBUG
					strcpy(szLog, "Ending active call, swapping back to held call ...");
					LogString(szLog, DEBUG_INFO);
#endif
				}
				// otherwise let the built-in processor handle ending call and disconnecting BT audio
				break;
			} // CHUP
			case BVRA:
			{ 
				wLogFlag = DEBUG_VOICEDIAL;
				if (*pTail == '=')
				{
					if (*(pTail+1) == '0') // Turn off voice recognition (terminate audio link)
					{
						DisconnectAudio();
						// but make sure this command gets passed on
						bReturn = FALSE;
						dwErr = ERR_NOERR;
						break;
					}
					else if (*(pTail+1) == '1') // Start voice dial link
					{
#ifdef DEBUG_HERMES
						if (g_pfnChainCommand != NULL)
						{
							bReturn = g_pfnChainCommand(szCommand, cbCommand);

							sprintf(szLog, "Chain function returned %i", bReturn);
							LogString(szLog, DEBUG_CHAIN);
						}
#endif
						if (InitiateVoiceDial())
						{
							dwErr = ERR_OK;
							bReturn = TRUE;
						}
						else
						{
							bReturn = FALSE;
							dwErr = ERR_NOERR;
						}
					}
				}
				break;
			}
			case CKPD:
			{
				wLogFlag = DEBUG_VOICEDIAL;
				// GetRegSettings();
				// InitializeSession(TRUE);
				if (!g_bInCall && (g_dwCallSetup == 0) && (strncmp(pTail, "=200", 4) == 0))
				{
					if (InitiateVoiceDial())
					{
						dwErr = ERR_OK;
						bReturn = TRUE;
					}
					else
					{
						bReturn = FALSE;
						dwErr = ERR_NOERR;
					}
				}
				break;
			} // CKPD
			case NREC:
			{
				dwErr = ERR_OK; // we don't support noise reduction or echo canceling
				bReturn = TRUE;
				wLogFlag = DEBUG_INFO;
				break;
			} // NREC
			case CREG: // network registration
			{
				dwErr = ERR_OK;
				bReturn = TRUE;
				// g_bNeedExtraStatus = TRUE;
				if (*pTail == QUERY)
				{
					sprintf(szReply, "+CREG: 0,%i", (g_bRoaming) ? 5 : (g_bService) ? 1 : 2);
				}
				else if (! _strnicmp(pTail, szOptions, 2))
				{
					// TODO: implement 0-1 options to allow asking for unsolicited change responses
					strcpy(szReply, "+CREG: (0)");
				}
				wLogFlag = DEBUG_INFO;
				break;
			} // CREG
			case CLCC: // list of current calls
			{
				dwErr = ERR_OK;
				bReturn = TRUE;
				/*
				if (g_bDialingInitiated)
				{
					// HACKHACK: hoping to convince Audi that the call it just dialed is in progress so it will remain connected to phone
					SendATCommand("+CIEV: 3,2"); // callsetup=outgoing in progress
					// ConnectAudio();
				}
				*/
#ifdef USE_NOTIFY
				if (g_pNotify)
					g_pNotify->StartIndicators();
#endif
				OutputCurrentCallList();
				wLogFlag = DEBUG_INFO;
				bExtraCRLF = TRUE;
				break;
			}
			case CPAS: // current phone status
			{
				dwErr = ERR_OK;
				bReturn = TRUE;
#ifdef USE_NOTIFY
				if (g_pNotify)
					g_pNotify->StartIndicators();
#endif
				sprintf(szReply, "+CPAS: %i", (g_bInCall || g_bDialingInitiated) ? 4
							: (g_dwCallSetup == CALLSETUP_INCOMING) ? 3 : (g_dwCallSetup > CALLSETUP_INCOMING) ? 4 : 0);
				wLogFlag = DEBUG_INFO;
				bExtraCRLF = TRUE; // TODO: Verify that +CPAS: response is followed by extra \r\n
				break;
			} // CPAS
			case CGMI:
			{
				// no options - just return the Mfr string
				if (g_szMfrString == NULL)
					strcpy(szReply, "Microsoft");
				else
					sprintf(szReply, "%ls", g_szMfrString);
				bReturn = TRUE;
				dwErr = ERR_OK;
				wLogFlag = DEBUG_INFO;
				break;
			} // CGMI
			case CGMM:
			{
				// no options - just return the Model string
				if (g_szModelString == NULL)
				{
					if (g_bIsSmartPhone)
						strcpy(szReply, "SmartPhone");
					else
						strcpy(szReply, "PocketPC Phone Edition");
				}
				else
					sprintf(szReply, "%ls", g_szModelString);
				bReturn = TRUE;
				dwErr = ERR_OK;
				wLogFlag = DEBUG_INFO;
				break;
			} // CGMM
			case CGMR:
			{
				// no options - just return the Version string
				OSVERSIONINFO osVer;
				osVer.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
				if (GetVersionEx(&osVer))
					sprintf(szReply, "%i.%i.%i %ls", osVer.dwMajorVersion, osVer.dwMinorVersion,
							osVer.dwBuildNumber, osVer.szCSDVersion);
				else
					strcpy(szReply, "unknown");
				bReturn = TRUE;
				dwErr = ERR_OK;
				wLogFlag = DEBUG_INFO;
				break;
			} // CGMM
			case CPIN:
			{
				wLogFlag = DEBUG_SIM;
				if (*pTail == QUERY) // Query current SIM lock status
				{
					if (InitializeSIM())
					{
						DWORD dwPINState = SIM_LOCKEDSTATE_UNKNOWN;
						HRESULT hRes = SimGetPhoneLockedState(g_hSIM, &dwPINState);
						if (hRes == S_OK)
						{
							CHAR szPINText[16];
							dwErr = ERR_OK;

							switch (dwPINState)
							{
								case SIM_LOCKEDSTATE_READY:
									strcpy(szPINText, "READY");
									break;
								case SIM_LOCKEDSTATE_SIM_PIN:
									strcpy(szPINText, "SIM PIN");
									break;
								case SIM_LOCKEDSTATE_SIM_PUK:
									strcpy(szPINText, "SIM PUK");
									break;
								case SIM_LOCKEDSTATE_SIM_PIN2:
									strcpy(szPINText, "SIM PIN2");
									break;
								case SIM_LOCKEDSTATE_SIM_PUK2:
									strcpy(szPINText, "SIM PUK2");
									break;
								case SIM_LOCKEDSTATE_PH_SIM_PIN:
									strcpy(szPINText, "PH-SIM PIN");
									break;
//								case SIM_LOCKEDSTATE_PH_SIM_PUK:
//									strcpy(szPINText, "PH-SIM PUK");
//									break;
								case SIM_LOCKEDSTATE_PH_FSIM_PIN:
									strcpy(szPINText, "PH-FSIM PIN");
									break;
								case SIM_LOCKEDSTATE_PH_FSIM_PUK:
									strcpy(szPINText, "PH-FSIM PUK");
									break;
								case SIM_LOCKEDSTATE_PH_NET_PIN:
									strcpy(szPINText, "PH-NET PIN");
									break;
								case SIM_LOCKEDSTATE_PH_NET_PUK:
									strcpy(szPINText, "PH-NET PUK");
									break;
								case SIM_LOCKEDSTATE_PH_NETSUB_PIN:
									strcpy(szPINText, "PH-NETSUB PIN");
									break;
								case SIM_LOCKEDSTATE_PH_NETSUB_PUK:
									strcpy(szPINText, "PH-NETSUB PUK");
									break;
								case SIM_LOCKEDSTATE_PH_SP_PIN:
									strcpy(szPINText, "PH-SP PIN");
									break;
								case SIM_LOCKEDSTATE_PH_SP_PUK:
									strcpy(szPINText, "PH-SP PUK");
									break;
								case SIM_LOCKEDSTATE_PH_CORP_PIN:
									strcpy(szPINText, "PH-CORP PIN");
									break;
								case SIM_LOCKEDSTATE_PH_CORP_PUK:
									strcpy(szPINText, "PH-CORP PUK");
									break;
								default:
									dwErr = ERR_CMEERR+10; // TODO: return appropriate CME ERROR
									break;
							} // switch(dwPINState)
							if (dwErr == ERR_OK)
							{
								sprintf(szReply, "+CPIN: %s", szPINText);
							}
							bReturn = TRUE;
						} // SimGetPhoneLockedState
					} // InitializeSIM()
				} // if (*pTail == QUERY)
				else // pTail contains "=pin#[,newpin#]
				{
					if (InitializeSIM())
					{
						HRESULT hRes = S_OK;
						DWORD dwPIN = 0;
						DWORD dwNewPIN = 0;
						TCHAR szPIN[16];
						TCHAR szNewPIN[16];
						int nCount = sscanf(pTail, "=%i,%i", &dwPIN, &dwNewPIN);
						if (nCount == 2)
						{
							wsprintf(szPIN, _T("%i"), dwPIN);
							wsprintf(szNewPIN, _T("%i"), dwNewPIN);
							hRes = SimUnlockPhone(g_hSIM, szPIN, szNewPIN);
							dwErr = (hRes == S_OK) ? ERR_OK : dwErr = ERR_CMEERR+16;// wrong PIN
						}
						else if (nCount == 1)
						{
							wsprintf(szPIN, _T("%i"), dwPIN);
							hRes = SimUnlockPhone(g_hSIM, szPIN, NULL);
							dwErr = (hRes == S_OK) ? ERR_OK : dwErr = ERR_CMEERR+16;// wrong PIN
						}
						else
							dwErr = ERR_CMEERR+5; // SIM locked TODO: find correct err code
						bReturn = TRUE;
					}

				}
				break;
			} // case CPIN
			case CPMS:
			{
				DWORD dwMsgCount = 0;
				if (g_dwHandleCPMS == CMD_INTERNAL_ONLY)
					break;
				wLogFlag = DEBUG_MESSAGES;
				EnterCriticalSection(&csMsgList);
				// initialize after set command
				if (*pTail == QUERY) // Query current Preferred Message Store info
				{
					dwMsgCount = InitializeMAPI();
					// TODO: Include dummy message if count=0
					sprintf(szReply, "+CPMS: \"%s\",%i,%i", MSLIST[g_dwMSSelected], dwMsgCount, g_dwMsgMax[g_dwMSSelected]*4); // return used,total...
				}
				else if (! _strnicmp(pTail, szOptions, 2)) // Get Preferred Message Store options
				{
					strcpy(szReply, "+CPMS: (\"ME\",\"SM\")");
				}
				else if (! _strnicmp(pTail, "=\"ME\"", 5)) // Set Preferred Message Store to ActiveSync
				{
					g_dwMSSelected = MESSAGES_ME;
					dwMsgCount = InitializeMAPI();
					sprintf(szReply, "+CPMS: %i,%i", dwMsgCount, g_dwMsgMax[g_dwMSSelected]*4);
				}
				else if (! _strnicmp(pTail, "=\"SM\"", 5)) // Set Preferred Message Store to SMS
				{
					g_dwMSSelected = MESSAGES_SM;
					dwMsgCount = InitializeMAPI();
					sprintf(szReply, "+CPMS: %i,%i", dwMsgCount, g_dwMsgMax[g_dwMSSelected]*4);
				}
				LeaveCriticalSection(&csMsgList);
				dwErr = ERR_OK;
				bReturn = TRUE;
				bExtraCRLF = TRUE;
				break;
			} // case CPMS
			case CNMI:
			{
				//TODO: May need to support option 2 in nMsg to use "+CMT:..." reporting of new messages
				if (g_dwHandleCPMS == CMD_INTERNAL_ONLY)
					break;
				wLogFlag = DEBUG_INDICATORS;
				if (*pTail == QUERY) // query New Message Indicator current settings
					sprintf(szReply, "+CNMI: %i,%i,0,0,0", GetMapiNotifyMode(), GetMapiNotify());
				else if (! _strnicmp(pTail, szOptions, 2)) // query New Message Indicator notification options
					strcpy(szReply, "+CNMI: (0-3),(0,1),(0),(0),(0)");
				else // if (! _strnicmp(szCommand, "AT+CNMI=", 8)) // set New Message Indicator notification options
				{
					int nMode=0, nMsg=0, nBCast=0, nStat=0, nBfr=0;
					int nRet = sscanf(pTail, "=%i,%i,%i,%i,%i", &nMode, &nMsg, &nBCast, &nStat, &nBfr);
					if (nRet >= 2)
					{
#ifdef USE_NOTIFY
						if (g_pNotify)
						{
							if (nMsg)
								g_pNotify->StartMail(g_dwMSSelected);
							else
								g_pNotify->StopMail(g_dwMSSelected);
						}
#endif
						SetMapiNotify(g_dwMSSelected, nMsg, nMode);
						if (nRet == 2)
						{
							// So far we only see this format from new BMW module by Motorola
						}
					}
					szReply[0]=0;
				}
				dwErr = ERR_OK;
				bReturn = TRUE;
				break;
			}
			case CMGF:  // set Message Format to PDU mode
			{
				if (g_dwHandleCPMS == CMD_INTERNAL_ONLY)
					break;
				wLogFlag = DEBUG_MESSAGES;
				if (*pTail == QUERY)
				{
					sprintf(szReply, "+CMGF: %i", g_dwMsgFormat);
					// strcpy(szReply, "+CMGF: 0");
					dwErr = ERR_OK;
				}
				else if (! _strnicmp(pTail, szOptions, 2))
				{
					strcpy(szReply, "+CMGF: (0,1)");
					dwErr = ERR_OK;
				}
				else if (*pTail == '=') // This test should be more efficient than sscanf
				{
					if (*pTail+1 == '0')
						g_dwMsgFormat = MSG_FORMAT_PDU;
					else if (*pTail+1 == '1')
						g_dwMsgFormat = MSG_FORMAT_TEXT;
					else
						dwErr = ERR_ERROR;
				}
				/*
				else if (! _strnicmp(pTail, "=0", 2))
					dwErr = ERR_OK;
				*/
				else
					dwErr = ERR_ERROR;
				bReturn = TRUE;
				break;
			}
			// Read message record n
			case CMGR:
			{
				int nIndex = 0;
				if (g_dwHandleCPMS == CMD_INTERNAL_ONLY)
					break;
				if (sscanf(pTail, "=%i", &nIndex) == 1)
				{
					//TODO: Remove dummy message support, no longer needed
					if (nIndex <= DUMMY_MSG_COUNT)
					{
						dwErr = ERR_OK;
						strcpy(szReply,"+CMGR: 3,,27\r\n0004038122F200044021218150032B0C48656C6C6F20576F726C642E");
					}
					else if (ReadMapiMessage(nIndex))
						dwErr = ERR_OK;
					else
						dwErr = ERR_CMSERR + 500;
				}
				else
					dwErr = ERR_ERROR;
				wLogFlag = DEBUG_MESSAGES;
				bReturn = TRUE;
				bExtraCRLF = TRUE;
				break;
			}
			// Delete message
			case CMGD:
			{
				int nIndex = 0;
				if (g_dwHandleCPMS == CMD_INTERNAL_ONLY)
					break;
				if (sscanf(pTail, "=%i", &nIndex) == 1)
				{
					if (DeleteMapiMessage(nIndex))
						dwErr = ERR_OK;
					else
						dwErr = ERR_CMSERR + 500;
				}
				else
					dwErr = ERR_ERROR;
				wLogFlag = DEBUG_MESSAGES;
				bReturn = TRUE;
				break;
			}
			case CSQ:
			{
				wLogFlag = DEBUG_INDICATORS;
				sprintf(szReply, "+CSQ: %i,99", (DWORD) ((g_dwSignalLevel * 20) / 0xFFFF));
				dwErr = ERR_OK;
				bReturn = TRUE;
				break;
			}
			case CBC:
			{
				wLogFlag = DEBUG_INDICATORS;
				sprintf(szReply, "+CBC: 0,%i", g_dwBatteryLevel);
				dwErr = ERR_OK;
				bReturn = TRUE;
				break;
			}
			case CIND:
			{
				wLogFlag = DEBUG_INDICATORS;
				/* internal AG handles first three indicators, we do the new ones */
				if (g_dwHandleCIND > CMD_INTERNAL_ONLY)
				{
					if (*pTail == QUERY) // Get status indicators current state
					{
						//              ------1--------- -------2------ --------3---------- --------4-------- ------5--------- -------6------ ------------7------------- -------8--------
						// "\r\n+CIND: ("service",(0,1)),("call",(0,1)),("callsetup",(0-3)),("battchg",(0-5)),("signal",(0-5)),("roam",(0-1)),("chargerconnected",(0-1)),("message",(0-1))"
						BOOL bTempReportStatus = g_bReportStatus; // Get current status reporting setting (+CMER)
						DWORD dwBattLevel = 1;
						DWORD dwSignalLevel = 1;
						// need a semaphore for synchronization of threads
						EnterCriticalSection(&csStatus);
						g_bReportStatus = FALSE; // temporarily turn it off while we update status variables for this reply
						LeaveCriticalSection(&csStatus);
#ifdef USE_NOTIFY
						if (g_pNotify != NULL)
							g_pNotify->StartIndicators();
#endif
						if (InitializeTAPI() == ERR_NONE)
							UpdateIndicators();
						EnterCriticalSection(&csStatus);
						dwBattLevel = (g_dwBatteryLevel < 90) ? ((g_dwBatteryLevel + 10) / 20) : 5;
						dwSignalLevel = ((g_dwSignalLevel + 0x1999) * 5) / 0xFFFF;
						sprintf(szReply, "+CIND: %i,%i,%i,%i,%i,%i", g_bService, (g_bAnsweringCall)?1:g_bInCall, 
							(g_bAnsweringCall)?0:g_dwCallSetup, dwBattLevel, dwSignalLevel, g_bRoaming);
						g_bReportStatus = bTempReportStatus; // set global reporting back
						LeaveCriticalSection(&csStatus);
					}
					else if (! _strnicmp(pTail, szOptions, 2)) // Get list of status indicators supported
					{
						//              ------1--------- -------2------ --------3---------- -------4------- ------5--------- --------6-------- ------7-------
						// "\r\n+CIND: ("service",(0,1)),("call",(0,1)),("callsetup",(0-3)),("battchg",(0-5)),("signal",(0-5)),("message",(0-1)),("roam",(0-1))"
						strcpy(szReply, "+CIND: (\"service\",(0-1)),(\"call\",(0-1)),(\"callsetup\",(0-3)),(\"battchg\",(0-5)),(\"signal\",(0-5)),(\"roam\",(0-1))");
						EnterCriticalSection(&csStatus);
						g_bReportStatus = FALSE;
						LeaveCriticalSection(&csStatus);
						// InitializeSession(TRUE); // TESTING: does this cause some settings to be reset mid-session
					}
					dwErr = ERR_OK;
					bReturn = TRUE;
					bExtraCRLF = TRUE;
				}
				break;
			} // case CIND:

			// internal AT+CIND=? response is "\r\n+CIND: (\"service\",(0,1)),(\"call\",(0,1)),(\"callsetup\",(0-3))\r\n"
			case CMER:
			{
				wLogFlag = DEBUG_INDICATORS;
				// registry option to handle CMER or leave to internal (default=both)
				if (g_dwHandleCMER > CMD_INTERNAL_ONLY)
				{
					if (*pTail == QUERY)
					{
						EnterCriticalSection(&csStatus);
						sprintf(szReply, "+CMER: 3,0,0,%i", g_bReportStatus);
						LeaveCriticalSection(&csStatus);
					}
					else if (! _strnicmp(pTail, szOptions, 8)) // activate indicator status reporting
					{
						strcpy(szReply, "+CMER: (0,3),(0),(0),(0,1)");
						EnterCriticalSection(&csStatus);
						g_bReportStatus = FALSE;
						LeaveCriticalSection(&csStatus);
					}
					if (g_dwHandleCMER == CMD_LOCAL_ONLY) // if internal reporting not being done, we need to report service, call, and callsetup
						g_bNeedExtraStatus = TRUE;
				}
				//	activate indicator status reporting
				if ((*pTail == '=') && (*(pTail+1) != '?'))
				{
					int nMode=0, nKey=0, nDisp=0, nCount=0, nReportStatus=0;
					nCount = sscanf(pTail, "=%i,%i,%i,%i", &nMode, &nKey, &nDisp, &nReportStatus);
					if (nCount == 4)
					{
						if (g_dwHandleCMER > CMD_INTERNAL_ONLY)
						{
							EnterCriticalSection(&csStatus);
							g_bReportStatus = nReportStatus;
							LeaveCriticalSection(&csStatus);
						}
						if (nReportStatus)
						{
							if (!(g_dwFeatureMask & FM_EARLY_REGREAD))
							{
								GetRegSettings();
								g_bReportStatus = nReportStatus;
							}
#ifdef USE_NOTIFY
							if (g_pNotify != NULL)
								g_pNotify->StartIndicators();
#endif
							KickMonitorThread(); // To allow monitor thread to see that monitoring has been enabled
						}
#ifdef USE_NOTIFY
						else
						{
							if (g_pNotify != NULL)
								g_pNotify->StopIndicators();
						}
#endif
					}
				}
				if (g_dwHandleCMER == CMD_LOCAL_ONLY)
				{
					// We will handle reporting w/o internal, else
					// Let internal code also handle query and options
					dwErr = ERR_OK;
					bReturn = TRUE;
				}
				break;
			} // case CMER
			case CSCS:
			// "AT+CSCS=xx" Set character set to xx
			{
				wLogFlag = DEBUG_INFO;
				if (g_dwHandleCPBS == CMD_INTERNAL_ONLY)
					break;
				dwErr = ERR_OK;
				bReturn = TRUE;
				if (*pTail == QUERY)
				{
					strcpy(szReply, "+CSCS: ");
					if (g_wConvertToCharSet == CHARSET_UTF8)
						strcat(szReply, "\"UTF-8\"");
					else if (g_wConvertToCharSet == CHARSET_UCS2)
						strcat(szReply, "\"UCS2\"");
					else
						strcat(szReply, "\"GSM\"");
					bExtraCRLF = TRUE;
				}
				else if (! _strnicmp(pTail, szOptions, 2)) // what character sets are supported
				{
#ifdef DEBUG_UCS2
					strcpy(szReply, "+CSCS: (\"GSM\",\"UTF8\",\"UCS2\")");
#else
					strcpy(szReply, "+CSCS: (\"GSM\",\"UTF-8\",\"UTF8\",\"UCS2\")");
#endif
					bExtraCRLF = TRUE;
				}
				else if (! _strnicmp(pTail, "=\"UTF-8\"", 8))
				{
					g_wConvertToCharSet = CHARSET_UTF8;
				}
				else if (! _strnicmp(pTail, "=\"UTF8\"", 7))
				{
					//NOTE: Allowing alternate form of UTF-8, but we return "UTF-8" on query
					g_wConvertToCharSet = CHARSET_UTF8;
				}
				else if (! _strnicmp(pTail, "=\"UCS2\"", 7))
				{
					g_wConvertToCharSet = CHARSET_UCS2;
				}
				else if (! _strnicmp(pTail, "=\"GSM\"", 6))
				{
					g_wConvertToCharSet = CHARSET_GSM;
				}
				else
				{
					dwErr = ERR_ERROR;
					break;
				}
				break;
			}
			case CPBS:
			{
				wLogFlag = DEBUG_CONTACTS;
				if (g_dwHandleCPBS == CMD_INTERNAL_ONLY)
					break;
				dwErr = ERR_ERROR;
				if (*pTail == QUERY)
				{
					sprintf(szReply, "+CPBS: \"%s\"", PBLIST[g_dwPSSelected]);
					if (g_dwPbSwapMeSm && !g_dwReportSimMax) // TODO: Parrot 3200LS workaround - overridden by Report max for SIM
					{
						if (g_dwPSSelected == CONTACTS_ME)
							sprintf(szReply, "+CPBS: \"%s\"", PBLIST[CONTACTS_SM]);
						else if (g_dwPSSelected == CONTACTS_SM)
							sprintf(szReply, "+CPBS: \"%s\"", PBLIST[CONTACTS_ME]);
					}
					if (!(g_dwFeatureMask & FM_CPBS_NO_COUNT)) // Support devices that expect used, total as part of CPBS? query
					{
						CHAR szCount[16];
						szCount[0] = 0;
						switch (g_dwPSSelected)
						{
							case CONTACTS_ME:
							{
								DWORD nCountNumbers = CountContacts();
								DWORD dwCount = (g_dwMaxContacts[CONTACTS_ME] != NO_LIMIT) ? 
										(min(g_dwMaxContacts[CONTACTS_ME], nCountNumbers+2)) : nCountNumbers+2;
								sprintf(szCount, ",%i,%i", dwCount, dwCount);
								break;
							}
							case CONTACTS_LD:
							case CONTACTS_FD:
							case CONTACTS_ON:
							case CONTACTS_SM:
							{ 
								if (InitializeSIM())
								{
									DWORD dwUsed = 0;
									DWORD dwTotal = 0;
									DWORD dwLoc = SIM_PBSTORAGE_SIM;
									if (g_dwPSSelected == CONTACTS_LD)
										dwLoc = SIM_PBSTORAGE_LASTDIALING;
									else if (g_dwPSSelected == CONTACTS_FD)
										dwLoc = SIM_PBSTORAGE_FIXEDDIALING;
									else if (g_dwPSSelected == CONTACTS_ON)
										dwLoc = SIM_PBSTORAGE_OWNNUMBERS;

									HRESULT hr = SimGetPhonebookStatus(g_hSIM, dwLoc, &dwUsed, &dwTotal);
									if (hr == S_OK)
									{
										sprintf(szCount, ",%i,%i", (g_dwMaxContacts[g_dwPSSelected] != NO_LIMIT) ? min(g_dwMaxContacts[g_dwPSSelected],dwUsed) : dwUsed, 
											(g_dwMaxContacts[g_dwPSSelected] != NO_LIMIT) ? (min(g_dwMaxContacts[g_dwPSSelected], dwTotal)) : dwTotal);
									}
								}
								else // work around phones without SIMs
								{
									// Just say there are no more than 10 entries, then report each entry as empty
									sprintf(szCount, "0,10");
								}
								break;
							}
							case CONTACTS_DC:
							case CONTACTS_RC:
							case CONTACTS_MC:
							{
								HANDLE hPhone;
								HRESULT hr = PhoneOpenCallLog(&hPhone);
								if (hr == S_OK)
								{
									DWORD dwCount = 0;
									DWORD dwMaxCount = 0;
									hr = PhoneSeekCallLog(hPhone, CALLLOGSEEK_END, 0, &dwCount);
									dwMaxCount = (g_dwMaxContacts[g_dwPSSelected] != NO_LIMIT) ? (min(g_dwMaxContacts[g_dwPSSelected], (dwCount+1))) : dwCount+1;
									sprintf(szCount, ",%i,%i", dwMaxCount, dwMaxCount);
									dwErr = ERR_OK;
									PhoneCloseCallLog(hPhone);
								}
								break;
							}
							default:
								break;
						}
						strcat(szReply, szCount);
					}
					dwErr = ERR_OK;
					bExtraCRLF = TRUE;
				}
				else if (! _strnicmp(pTail, szOptions, 2))
				{
					strcpy(szReply, "+CPBS: (\"ME\",\"SM\",\"DC\",\"RC\",\"MC\",\"LD\",\"FD\",\"ON\")");
					dwErr = ERR_OK;
					bExtraCRLF = TRUE;
				}
				else if (! _strnicmp(pTail, "=\"ME\"", 5)) // set Phonebook storage to Mobile Equip (memory)
				{
					g_dwPSSelected = CONTACTS_ME;
					if (g_dwPbSwapMeSm && !g_dwReportSimMax) // TODO: Parrot 3200LS workaround 
						g_dwPSSelected = CONTACTS_SM;
					dwErr = ERR_OK;
				}
				else if (! _strnicmp(pTail, "=\"SM\"", 5)) // set Phonebook storage to SIM
				{
					g_dwPSSelected = CONTACTS_SM;
					if (g_dwPbSwapMeSm && !g_dwReportSimMax) // TODO: Parrot 3200LS workaround 
						g_dwPSSelected = CONTACTS_ME;
					dwErr = ERR_OK;
				}
				else if (! _strnicmp(pTail, "=\"DC\"", 5)) // set Phonebook storage to Dialed Calls
				{
					g_dwPSSelected = CONTACTS_DC;
					dwErr = ERR_OK;
				}
				else if (! _strnicmp(pTail, "=\"RC\"", 5)) // set Phonebook storage to Received Calls
				{
					g_dwPSSelected = CONTACTS_RC;
					dwErr = ERR_OK;
				}
				else if (! _strnicmp(pTail, "=\"MC\"", 5)) // set Phonebook storage to Missed Calls
				{
					g_dwPSSelected = CONTACTS_MC;
					dwErr = ERR_OK;
				}
				else if (! _strnicmp(pTail, "=\"LD\"", 5)) // set Phonebook storage to SIM Last Dialed Calls
				{
					g_dwPSSelected = CONTACTS_LD;
					dwErr = ERR_OK;
				}
				else if (! _strnicmp(pTail, "=\"FD\"", 5)) // set Phonebook storage to SIM Fixed Dialing numbers
				{
					g_dwPSSelected = CONTACTS_FD;
					dwErr = ERR_OK;
				}
				else if (! _strnicmp(pTail, "=\"ON\"", 5)) // set Phonebook storage to SIM Own Numbers
				{
					g_dwPSSelected = CONTACTS_ON;
					dwErr = ERR_OK;
				}
				bReturn = TRUE;
				break;
			}
			case CPBR:
			{
				wLogFlag = DEBUG_CONTACTS;
				if (g_dwHandleCPBS == CMD_INTERNAL_ONLY)
					break;
				bExtraCRLF = TRUE;
				if (! _strnicmp(pTail, szOptions, 2)) // Read Phonebook records - get options
				{
					dwErr = ERR_CMEERR+500; // CME ERROR: 500 as default, override on success
					switch (g_dwPSSelected)
					{
						case CONTACTS_ME:
						{
							DWORD nCountNumbers = CountContacts();
							// if (nCountNumbers > 0) // FIX: 1.20 RTW: Parrot won't read recent call lists if it gets CME ERROR when Outlook phonebook limited to 0
							{
								// BWM bug - does not attempt to read the last phonebook entry reported
								// actual name and number lengths for Pocket Outlook are 1023, reply with more conservative number
								// TODO: Restrict returned info to 32 chars for numbers
								sprintf(szReply, "+CPBR: (%i-%i),32,%i", (nCountNumbers > 0) ? 1 : 0, (g_dwMaxContacts[CONTACTS_ME] != NO_LIMIT) ? 
									(min(g_dwMaxContacts[CONTACTS_ME], nCountNumbers+2)) : nCountNumbers+2, g_dwMaxPBName);
								dwErr = ERR_OK;
							}
							break;
						}
						case CONTACTS_LD:
						case CONTACTS_FD:
						case CONTACTS_ON:
						case CONTACTS_SM:
						{ 
							if (InitializeSIM())
							{
								SIMCAPS sc;
								DWORD dwUsed = 0;
								DWORD dwTotal = 0;
								DWORD dwLoc = SIM_PBSTORAGE_SIM;
								if (g_dwPSSelected == CONTACTS_LD)
									dwLoc = SIM_PBSTORAGE_LASTDIALING;
								else if (g_dwPSSelected == CONTACTS_FD)
									dwLoc = SIM_PBSTORAGE_FIXEDDIALING;
								else if (g_dwPSSelected == CONTACTS_ON)
									dwLoc = SIM_PBSTORAGE_OWNNUMBERS;

								HRESULT hr = SimGetPhonebookStatus(g_hSIM, dwLoc, &dwUsed, &dwTotal);
								if (g_dwReportSimMax && (g_dwPSSelected == CONTACTS_SM)) // TODO: Parrot 3200LS workaround - overrides swap ME-SM workaround
								{
									DWORD dwMeCount = CountContacts();
									if (dwMeCount > dwTotal)
										dwTotal = dwMeCount;
								}
								if (hr == S_OK)
								{
									hr = SimGetDevCaps(g_hSIM, SIM_CAPSTYPE_PBENTRYLENGTH, &sc);
									sprintf(szReply, "+CPBR: (%i-%i),%i,%i", (g_dwMaxContacts[g_dwPSSelected] != 0) ? 1 : 0, 
										(g_dwMaxContacts[g_dwPSSelected] != NO_LIMIT) ? (min(g_dwMaxContacts[g_dwPSSelected], dwTotal)) : dwTotal,
										(!hr) ? sc.dwMaxPBEAddressLength : 32, (!hr) ? sc.dwMaxPBETextLength : 32);
									dwErr = ERR_OK;
								}
							}
							if (dwErr != ERR_OK) // work around phones without SIMs
							{
								// Just say there are no more than 10 entries, then report each entry as empty
								sprintf(szReply, "+CPBR: (%i-%i),16,32", min(g_dwMaxContacts[g_dwPSSelected], 1),
									min(g_dwMaxContacts[g_dwPSSelected], 10));
								dwErr = ERR_OK;
							}
							break;
						}
						case CONTACTS_DC:
						case CONTACTS_RC:
						case CONTACTS_MC:
						{
							HANDLE hPhone;
							HRESULT hr = PhoneOpenCallLog(&hPhone);
							if (hr == S_OK)
							{
								DWORD dwCount = 0;
								hr = PhoneSeekCallLog(hPhone, CALLLOGSEEK_END, 0, &dwCount);
								sprintf(szReply, "+CPBR: (1-%i),32,64", (g_dwMaxContacts[g_dwPSSelected] != NO_LIMIT) ?
									(min(g_dwMaxContacts[g_dwPSSelected], (dwCount+1))) : dwCount+1);
								dwErr = ERR_OK;
								PhoneCloseCallLog(hPhone);
							}
							// int nCount = CountCallContacts(g_dwPSSelected);
							// sprintf(szReply, "+CPBR: (%i-%i),32,64",(nCount > 0) ? 1 : 0, nCount);
							dwErr = ERR_OK;

							break;
						}
						default:
							break;
					}
					bReturn = TRUE;
				}

				else // if (! _strnicmp(szCommand, "AT+CPBR=", 8)) // Read Phonebook records
				{
					int nRecordEnd = 0;
					int nRecord = 0;
					int nCount = sscanf(pTail, "=%i,%i", &nRecord, &nRecordEnd);
					dwErr = ERR_ERROR;
					bReturn = TRUE;

					if (nCount > 0)
					{
						if ((nCount == 2) && (nRecordEnd < nRecord))
						{
							dwErr = ERR_ERROR;
						}
						else if ((g_dwMaxContacts[g_dwPSSelected] == NO_LIMIT) || ((DWORD) nRecord <= g_dwMaxContacts[g_dwPSSelected]))
						{
							if ((g_dwMaxContacts[g_dwPSSelected] != NO_LIMIT) && ((DWORD) nRecordEnd > g_dwMaxContacts[g_dwPSSelected]))
								nRecordEnd = g_dwMaxContacts[g_dwPSSelected];
							switch (g_dwPSSelected)
							{
								case CONTACTS_ME:
									ReadContacts(nRecord, nRecordEnd ? nRecordEnd : nRecord);
									break;
								case CONTACTS_LD:
								case CONTACTS_FD:
								case CONTACTS_ON:
								case CONTACTS_SM:
								{
									DWORD dwLoc = SIM_PBSTORAGE_SIM;
									if (g_dwPSSelected == CONTACTS_LD)
										dwLoc = SIM_PBSTORAGE_LASTDIALING;
									else if (g_dwPSSelected == CONTACTS_FD)
										dwLoc = SIM_PBSTORAGE_FIXEDDIALING;
									else if (g_dwPSSelected == CONTACTS_ON)
										dwLoc = SIM_PBSTORAGE_OWNNUMBERS;
									ReadSIMContacts(dwLoc, nRecord, nRecordEnd ? nRecordEnd : nRecord);
									break;
								}
								default:
									ReadCallContacts(nRecord, nRecordEnd ? nRecordEnd : nRecord, g_dwPSSelected);
									break;
							}
							dwErr = ERR_OK;
						}
					}

				}
				break;
			}
			case COPS: // return current operator
			{
				int iMode = 0; // automatically select network
				int iFormat = 0;
				wLogFlag = DEBUG_INFO;
				if (*pTail == QUERY)
				{
					TCHAR szOperator[48] = {0};
					if (GetOperator(szOperator))
					{
						sprintf(szReply, "+COPS: %i,0,\"%S\"", iMode, szOperator);
						dwErr = ERR_OK;
						bExtraCRLF = TRUE;
					}
					else
					{
						dwErr = ERR_ERROR;
					}
				}
				else if (sscanf(pTail, "=%i,%i", &iMode, &iFormat) > 0)
				{
					//TODO: Set mode and format as globals
					dwErr = ERR_OK;
				}
				else
					dwErr = ERR_CMEERR + 11;
				bReturn = TRUE;
				break;
			} // COPS
			case CIMI:
			{
				wLogFlag = DEBUG_INFO;
				CHAR szIMSI[32];
				DWORD dwRet = GetIMSI(szIMSI, sizeof(szIMSI));
				if (dwRet == 0)
				{
					strcpy(szReply, szIMSI);
					dwErr = ERR_OK;
				}
				else
					dwErr = ERR_CMEERR + 14;
				bReturn = TRUE;
				break;
			}
			case CGSN:
			{
				wLogFlag = DEBUG_INFO;
				CHAR szIMEI[32];
				memset(szIMEI, 0, sizeof(szIMEI));
				DWORD dwRet = GetIMEI(szIMEI, sizeof(szIMEI));
				if (dwRet == 0)
				{
					strcpy(szReply,szIMEI);
					dwErr = ERR_OK;
				}
				else
					dwErr = ERR_CMEERR + 22;
				bReturn = TRUE;
				break;
			}
			case CNUM: // return ME voice number
			{
				wLogFlag = DEBUG_INFO;
				CHAR szMyNum[MAX_PATH];
				BOOL bGotNumber = GetMyNumber(szMyNum, MAX_PATH);
				dwErr = ERR_OK;
				if (bGotNumber == TRUE)
					sprintf(szReply, "+CNUM: \"My Mobile #\",\"%s\",17,8,4", szMyNum);
				else
					dwErr = ERR_CMEERR + 14;
				bReturn = TRUE;
				break;
			} // CNUM
			case CCLK: // only support reading clock, not setting it
			{
				wLogFlag = DEBUG_INFO;
				if (*pTail == QUERY)
				{
					SYSTEMTIME st;
					TIME_ZONE_INFORMATION tz;
					GetLocalTime(&st);
					GetTimeZoneInformation(&tz);
					if (tz.Bias + tz.DaylightBias >= 0)
					{
						sprintf(szReply, "+CCLK: \"%2.2d/%2.2d/%2.2d,%2.2d:%2.2d:%2.2d+%2.2d\"", st.wYear-2000,
							st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, (tz.Bias + tz.DaylightBias)/60);
					}
					else
					{
						sprintf(szReply, "+CCLK: \"%2.2d/%2.2d/%2.2d,%2.2d:%2.2d:%2.2d%2.2d\"", st.wYear-2000,
							st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, (tz.Bias + tz.DaylightBias)/60);
					}
					dwErr = ERR_OK;
				}
				else
					dwErr = ERR_CMEERR + 3;
				bReturn = TRUE;
				break;
			} //CCLK
			case BMWJUNK:
			// garbage string from new factory module from BMW, just kill it
			{
#ifdef DEBUG
				sprintf(szLog, "Garbage found for %i bytes", cbCommand);
				LogString(szLog, DEBUG_INFO);
#endif
				dwErr = ERR_NOERR;
				bReturn = FALSE;
				break;
			} // BMWJUNK
			default:
			{
				dwErr = ERR_NOERR;
				bReturn = FALSE;
				if (! _strnicmp(szCommand, "ATD", 3))
				{
					// default handling of generic ATD
					// AddTempCall();
					g_bDialingInitiated = TRUE;
					if (g_dwFeatureMask & FM_RESUME_ON_DIAL)
						ResumeDevice();
					// see if it is the magic number to initiate voice dial
					if ((g_szInitiateVoiceDialNumber != NULL) && (cbCommand >= (strlen(g_szInitiateVoiceDialNumber)+3)))
					{
						if (! _strnicmp(szCommand+3, g_szInitiateVoiceDialNumber, strlen(g_szInitiateVoiceDialNumber)))
						{
							wLogFlag = DEBUG_VOICEDIAL;
							g_bDialingInitiated = FALSE;
							if (!g_bInCall && (g_dwCallSetup == 0))
							{
								if (InitiateVoiceDial())
								{
									dwErr = ERR_OK;
									bReturn = TRUE;
								}
							}
						}
					}
				}
				break;
			}
		} // switch *pdwCmd
		if (szReply[0] != 0)
		{
			AddLineToResponse(szReply, FALSE);
		}
		if (dwErr != ERR_NOERR)
		{
			CHAR szOK[] = {'O','K',0};
			CHAR szCMEErr[] = {'+','C','M','E',' ','E','R','R','O','R',':',' ',0};
			CHAR szCMSErr[] = {'+','C','M','S',' ','E','R','R','O','R',':',' ',0};
			CHAR szErr[] = {'E','R','R','O','R',0};

			if (dwErr == ERR_OK)
			{
				strcpy(szReply, szOK);
				if (bExtraCRLF)
					AddLineToResponse("", FALSE);
			}
			else if (dwErr == ERR_ERROR)
			{
				InitializeResponse();
				strcpy(szReply, szErr);
			}
			else if (dwErr >= ERR_CMEERR && dwErr < ERR_CMSERR)
			{
				InitializeResponse();
				sprintf(szReply, "%s%i", szCMEErr, dwErr - ERR_CMEERR);
			}
			else if (dwErr >= ERR_CMSERR && dwErr < ERR_CMSERR+1000)
			{
				InitializeResponse();
				sprintf(szReply, "%s%i", szCMSErr, dwErr - ERR_CMSERR);
			}
			AddLineToResponse(szReply, TRUE); // Second param=TRUE tells it this is final addition, send response to HF device
		}
		memset(szCmd, 0, 5);
	}
	else // cbCommand < 7
	{
		if ((cbCommand = 2) && (*szCommand == ';'))
		{
			bReturn = TRUE;
		}
		if (! _strnicmp(szCommand, "ATA", 3))
		{
			//Report that we have an active call now
			if (g_dwCallSetup == CALLSETUP_INCOMING)
			{
				g_bAnsweringCall = TRUE;
				// g_dwCallSetup = CALLSETUP_NONE;
				// g_bInCall = TRUE;
			}
			if (g_dwInBandRing)
			{
				DisconnectAudio();
			}
		}
	}
	// If we haven't handled command, give chain extension a chance
	if (!bReturn && (g_pfnChainCommand != NULL))
	{
		bReturn = g_pfnChainCommand(szCommand, cbCommand);

#ifdef DEBUG
		sprintf(szLog, "Chain function returned %i", bReturn);
		LogString(szLog, DEBUG_CHAIN);
#endif
	}
	if (!(g_dwFeatureMask & (FM_CS_PROTECT_RESP | FM_CS_NOPROTECT)))
		LeaveCriticalSection(&csCommand); // TEST: 1.21: Protecting response queue instead - it's OK to have unsolicited response within command response
#ifdef DEBUG
	sprintf(szLog, "Returning %i for %s", bReturn, szCommand);
	LogString(szLog, DEBUG_REQUESTS);
#endif
    return bReturn;
}


/* NOTES
To read roam/home status call lineGetRegisterStatus
To read SMS messages call SimReadMessage 
   or use CEMAPI to read the SMS message store - and get notifications
To read phonebook call SimReadPhonebookEntry or IMAPISession::OpenAddressBook
To get battery status call GetSystemPowerStatusEx2
To get signal call lineGetLineDevStatus (also has Roam and battery info)
RequestPowerNotifications
*/